
package analyse;

import graphik.EinfachesFenster;
import java.lang.reflect.*;
import java.util.HashSet;
import java.util.Set;

/**
 * Analysiert eine Klasse mittels Reflection auf ihre Elemente und speichert
 * den Klassennamen, die Exemplarvariablen, Konstruktoren und Methoden formatiert
 * ab.
 * Die Informationen können daraufhin ein einem Fenster im UML-Stil visualisiert
 * werden.
 * @author Dennis Wittkoetter <dennis.wittkoetter@hs-osnabrueck.de>
 */
public class Analysator {
    private EinfachesFenster fenster;
    private Class klasse;
    private String klassenname;
    private Set<String>exemplarVars;
    private Set<String>konstruktoren;
    private Set<String>methoden;
    
    private boolean DEBUG = false;
      
    /**
     * Analysiert die Klasse mit dem uebergebenen Klassennamen.
     * @param klassenname 
     *      Der Name der zu analysierenen Klasse.
     */
    public Analysator(String klassenname) {
        this.klassenname = "";
        exemplarVars = new HashSet<>();
        konstruktoren = new HashSet<>();
        methoden = new HashSet<>();
        
        try {
            klasse = Class.forName(klassenname);
            
            analysiereKlassenname();    //den Klassennamen per Reflection ermitteln
            analysiereExemplarVariablen();  //die Exemplarvariablen ermitteln abspeichern
            analysiereKonstruktoren();  //die Konstruktoren ermitteln und abspeichern
            analysiereMethoden();   //die Methoden ermitteln und abspeichern
        }
        catch (ClassNotFoundException e) {
            System.out.println("Diese Klasse existiert nicht!");
        }
    }
        
    /**
     * Gibt die analysierte Klasse im UML-Stil in einem separaten Fenster aus.
     * Die Fenstergroesse ist dabei abhaengig von der Anzahl und Breite der
     * anzuzeigenen Textzeilen.
     */
    public void malen() {
        if (klasse == null)   //Class-Objekt darf nicht leer sein (Analyse wurde nicht durchgefuehrt!)
            return;
            
        fenster = new EinfachesFenster();   //Fenster erzeugen

        //Koordinatenwerte initialisieren
        final int xPos_text = 10;   //Textabstand vom linken Fensterrand in px
        final int abstand_TextUndRahmen = 4;    //Abstand zwischen Text und Rahmen in px
        final int abstand_RahmenUndFenster = 4; //Abstand zwischen Rahmen und Fenster in px
        final int xPos_Linie_Links = xPos_text-abstand_TextUndRahmen;   //Rahmenabstand vom linken Fensterrand
        
        //variable Koordinaten
        int xPos_Linie_Rechts;  //Rahmenabstand vom rechten Fensterrand        
        int yPos_Trennlinie;    //Position der Trennlinie zwischen Variablen und Methoden
        int zeilenhoehe = fenster.texthoehe(klassenname);   //Hoehe einer Textzeile
        int zeilenZaehler = 2;  //Anzahl der Textzeilen (Start bei 2)
        int maxTextbreite = fenster.textbreite(klassenname);  //max. Textbreite     
        
        //Zeilen im Voraus hochzaehlen, wenn Klasse Interface oder abstrakt ist
        if (klasse.isInterface() || Modifier.isAbstract(klasse.getModifiers()))
            zeilenZaehler++;
                  
        //Startwert fuer Bewegungen auf der y-Achse festlegen
        //Start bei den Exemplarvariablen. Die restlichen Elemente werden spaeter
        //dynamisch hinzugefuegt.
        int yPos = abstand_RahmenUndFenster+(zeilenZaehler*zeilenhoehe);
        
        //Exemplarvariablen auslesen und schreiben
        for (String exVar : exemplarVars) {
            fenster.schreibe(xPos_text, yPos, exVar);
            int dieseTextbreite = fenster.textbreite(exVar);
            if (dieseTextbreite > maxTextbreite)
                maxTextbreite = dieseTextbreite;

            yPos += zeilenhoehe;
            zeilenZaehler++;
        }

        //Position der Trennlinie merken
        yPos_Trennlinie = yPos-zeilenhoehe/2;
        yPos += 5;

        //Konstruktoren auslesen und schreiben
        for (String konstruktor : konstruktoren) {
            fenster.schreibe(xPos_text, yPos, konstruktor);
            int dieseTextbreite = fenster.textbreite(konstruktor);
            if (dieseTextbreite > maxTextbreite)
                maxTextbreite = dieseTextbreite;

            yPos += zeilenhoehe;
            zeilenZaehler++;
        }

        //Methoden auslesen und schreiben 
        for (String methode : methoden) {
            fenster.schreibe(xPos_text, yPos, methode);
            int dieseTextbreite = fenster.textbreite(methode);
            if (dieseTextbreite > maxTextbreite)
                maxTextbreite = dieseTextbreite;

            yPos += zeilenhoehe;
            zeilenZaehler++;
        }
        
        //ausstehende Koordinaten errechnen
        xPos_Linie_Rechts = xPos_text+maxTextbreite+abstand_TextUndRahmen;
        int fensterbreite = xPos_Linie_Rechts+abstand_RahmenUndFenster;
        int fensterhoehe = zeilenZaehler*zeilenhoehe;

        //obere Rahmenlinie ziehen
        fenster.linie(xPos_Linie_Links, abstand_RahmenUndFenster, 
                      xPos_Linie_Rechts, abstand_RahmenUndFenster);

        //y-Position der Elemente im Kopfbereich
        int yPos2 = zeilenhoehe;
        
        //Klasse als Interface kennzeichnen
        if (klasse.isInterface()) {
            String str = "<<interface>>";
            int xPos_Interface = (fensterbreite/2)-(fenster.textbreite(str)/2);
            fenster.schreibe(xPos_Interface, yPos2, str);
            yPos2 += zeilenhoehe;
            //zeilenZaehler++;
        }
        //KLasse als abstarkt kennzeichnen
        else if (Modifier.isAbstract(klasse.getModifiers())) {
            String str = "<<abstract>>";
            int xPos_Interface = (fensterbreite/2)-(fenster.textbreite(str)/2);
            fenster.schreibe(xPos_Interface, yPos2, str);
            yPos2 += zeilenhoehe;
            //zeilenZaehler++;
        }
        
        //Klassennamen
        int xPos_Klassenname = (fensterbreite/2)-(fenster.textbreite(klassenname)/2);
        fenster.schreibe(xPos_Klassenname, yPos2, klassenname);

        //Trennlinie zwischen Klassenname und Exemplarvariablen
        fenster.linie(xPos_Linie_Links, yPos2+5, 
                      xPos_Linie_Rechts, yPos2+5);

        //Trennlinie zwischen Exemplarvariablen und Kontruktoren / Methoden ziehen
        fenster.linie(xPos_Linie_Links, yPos_Trennlinie, 
                      xPos_Linie_Rechts, yPos_Trennlinie);

        //untere Rahmenlinie ziehen
        fenster.linie(xPos_Linie_Links, yPos-zeilenhoehe+5, 
                      xPos_Linie_Rechts, yPos-zeilenhoehe+5);

        //linke Rahmenlinie ziehen
        fenster.linie(xPos_Linie_Links, abstand_RahmenUndFenster, 
                      xPos_Linie_Links, yPos-zeilenhoehe+5);

        //rechte Rahmenlinie ziehen
        fenster.linie(xPos_Linie_Rechts, abstand_RahmenUndFenster, 
                      xPos_Linie_Rechts, yPos-zeilenhoehe+5);

        //Fenstergroesse setzen (Fenster wird angezeigt)
        fenster.setGroesse(fensterbreite, fensterhoehe);
    }
    
    /**
     * Ermittelt den Namen der Klasse aus der Variablen 'klasse'
     * und schreibt ihn ins Fenster.
     * @return 
     *      Den Namen der Klasse.
     */
    private void analysiereKlassenname() {
        this.klassenname = cutPackagePath(klasse.getName());
            
        //Debug-Ausgaben
        printDebugSysos("Klassenname:"); //Test
        printDebugSysos(this.klassenname);
        printDebugSysos("------------------------");
    }
       
    /**
     * Ermittelt alle Exemplarvariablen aus der Variablen 'klasse'
     * und schreibt sie formatiert ins Fenster.
     * @return 
     *      Die formatierten Strings aller Exemplarvariablen als Set. 
     */
    private void analysiereExemplarVariablen() {
        StringBuilder sb;
        Class<?> type;
        Field[] exVars = klasse.getDeclaredFields();

        printDebugSysos("Exemplarvariabl(en):"); //Debug
        for (Field f : exVars) {
            if (f.getDeclaringClass().equals(klasse)) { //nur lokale Variablen beruecksichtigen
                sb = new StringBuilder();

                f.setAccessible(true);
                int mod = f.getModifiers();

                if (isVisible(mod)) {   //keine exotischen modifier anzeigen
                    String name = f.getName();
                    type = f.getType();

                    //Modifier-Symbol einfuegen
                    sb.append(addModifierToObjName(mod, name));
                    
                    //Rueckgabetyp anhaengen
                    if (type != null) {
                        sb.append(':');
                        sb.append(type.getSimpleName());
                    }
                    
                    exemplarVars.add(sb.toString());
                    printDebugSysos(sb.toString());
                }
            }

        }
        printDebugSysos("------------------------");
    }
    
    /**
     * Ermittelt alle Konstruktoren aus der Variablen 'klasse'
     * und schreibt formatiert sie ins Fenster.
     */
    private void analysiereKonstruktoren() {
        StringBuilder sb;
        Constructor[] kon = klasse.getDeclaredConstructors();

        printDebugSysos("Konstruktor(en):"); //Test
        for (Constructor k : kon) {
            if (k.getDeclaringClass().equals(klasse)) { //nur lokale Variablen beruecksichtigen
                sb = new StringBuilder();

                k.setAccessible(true);
                int mod = k.getModifiers();

                if (isVisible(mod)) {   //keine exotischen modifier anzeigen
                    String name = k.getName(); 

                    sb.append(addModifierToObjName(mod, name));
                    sb.append("(");
                    sb.append(getParameterTypes(k));            
                    sb.append(")");
                    
                    konstruktoren.add(sb.toString());
                    printDebugSysos(sb.toString()); //Test
                }
            }
        }
        printDebugSysos("------------------------");
    }
    
    /**
     * Ermittelt alle Methoden aus der Variablen 'klasse'
     * und schreibt formatiert sie ins Fenster.
     */
    private void analysiereMethoden() {
        StringBuilder sb;
        Class<?> retType;
        Method[] meth = klasse.getDeclaredMethods();
        
        printDebugSysos("Methode(n):");  //Test
        for (Method m : meth) {
            if (m.getDeclaringClass().equals(klasse)) { //nur lokale Variablen beruecksichtigen
                sb = new StringBuilder();

                m.setAccessible(true);   
                int mod = m.getModifiers();

                if (isVisible(mod)) {   //keine exotischen modifier anzeigen
                    String name = m.getName();
                    retType = m.getReturnType();

                    //Modifier-Symbol einfuegen
                    sb.append(addModifierToObjName(mod, name));
                    sb.append("(");
                    sb.append(getParameterTypes(m));            
                    sb.append(")");
                    
                    //Rueckgabetyp anhaengen
                    if (retType != null) {
                        sb.append(':');
                        sb.append(retType.getSimpleName());
                    }

                    methoden.add(sb.toString());
                    printDebugSysos(sb.toString());  //Test
                }
            }
        }
        printDebugSysos("------------------------");
    }
    
    
    /**
     * Liefert die Parameter-Typen des angegebenen Konstruktors als String
     * @param k
     *      Konstruktor
     * @return
     *      Die Parameter-Typen des Konstruktors als String, durch Kommas 
     *      getrennt.
     *      Liefert einen leeren String, falls keine Parameter definiert wurden.
     */
    private String getParameterTypes(Constructor k) {
        StringBuilder sb = new StringBuilder();

        for (int i=0; i<k.getParameterTypes().length; i++) {
            Class<?> param = k.getParameterTypes()[i];
            sb.append(cutPackagePath(param.getSimpleName()));
            if (i < k.getParameterTypes().length-1)
                sb.append(",");
        }
        return sb.toString();
    }
      
    /**
     * Liefert die Parameter-Typen des angegebenen Konstruktors als String
     * @param m
     *      Konstruktor
     * @return
     *      Die Parameter-Typen des Konstruktors als String, durch Kommas 
     *      getrennt.
     *      Liefert einen leeren String, falls keine Parameter definiert wurden.
     */
    private String getParameterTypes(Method m) {
        StringBuilder sb = new StringBuilder("");
        
        if (m.getParameterTypes().length == 0) {
            sb.append("void");
            return sb.toString();
        }
        
        for (int i=0; i<m.getParameterTypes().length; i++) {
            Class<?> param = m.getParameterTypes()[i];
            sb.append(cutPackagePath(param.getSimpleName()));
            if (i < m.getParameterTypes().length-1)
                sb.append(",");
        }
        return sb.toString();
    }
    
    /**
     * Entfernt den Package-Pfad vor einem beliebigen Klassennamen.
     * @param s
     *      Der zu kürzene String in der Form <Package-Pfad>.Klasse.
     * @return 
     *      Der Klassenname aus dem uebergebenen String.
     */
    private String cutPackagePath(String s) {
        StringBuilder sb = new StringBuilder(s);
        
        while(sb.indexOf(".") >= 0) {
            sb.delete(0, sb.indexOf(".")+1);
        }
                
        return sb.toString();
    }
        
    /**
     * Setzt das passende Symbol fuer den uebergebenen modifier vor den dazugehoerigen
     * Namen. Der Objekt-Name wird ausserdem formatiert.
     * @param modifier
     *      Der Integer-Wert des umzuwandelnen modifiers.
     * @param objName
     *      Der Name der Variablen, des Konstruktors oder der Methode des
     *      dazugehoerigen modifiers.
     * @return 
     *      Der Objekt-Name mit davor angehaengtem modifier-Symbol.
     */
    private String addModifierToObjName(int modifier, String objName) {
        StringBuilder out = new StringBuilder();
                 
        if (Modifier.isPublic(modifier))
            out.append('+');
        else if (Modifier.isProtected(modifier))
            out.append('#');
        else if (Modifier.isPrivate(modifier))
            out.append('-');
        
        if (Modifier.isFinal(modifier))
            out.append("final ");
        if (Modifier.isStatic(modifier))
            out.append("static ");

        out.append(cutPackagePath(objName));
 
        return out.toString(); 
    }
    
    /**
     * Liefert true, wenn der modifier der dazugehoerigen Variablen, 
     * Konstruktoren oder Methoden als sichtbar deklariert wurde.
     * @param modifier
     *      Der zu pruefende modifier.
     * @return 
     *      Liefert 'true' wenn der modifier sichtbar ist, sonst 'false'.
     */
    private boolean isVisible(int modifier) {
        if( Modifier.isPrivate(modifier)
        ||  Modifier.isProtected(modifier)
        ||  Modifier.isPublic(modifier) ) {
            return true;
        }
        
        return false;
    }
    
    /**
     * Schreibt Ausgaben in die Konsole, wenn DEBUG-Flag gesetzt wurde.
     * @param out 
     *      Die auszugebende Textzeile.
     */
    private void printDebugSysos(String out) {
        if (DEBUG) {
            System.out.println(out);
        }
    }
}
